package sample;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Iterator;

import rescuecore2.worldmodel.EntityID;
import rescuecore2.worldmodel.ChangeSet;
import rescuecore2.messages.Command;
import rescuecore2.misc.geometry.GeometryTools2D;
import rescuecore2.misc.geometry.Line2D;
import rescuecore2.misc.geometry.Point2D;
import rescuecore2.log.Logger;

import rescuecore2.standard.entities.Area;
import rescuecore2.standard.entities.Blockade;
import rescuecore2.standard.entities.Building;
import rescuecore2.standard.entities.StandardEntity;
import rescuecore2.standard.entities.StandardEntityURN;
import rescuecore2.standard.entities.AmbulanceTeam;
import rescuecore2.standard.entities.Human;
import rescuecore2.standard.entities.Civilian;
import rescuecore2.standard.entities.Refuge;
import rescuecore2.standard.messages.AKSpeak;

/**
   A sample ambulance team agent.
 */
public class SampleAmbulanceTeam extends AbstractSampleAgent<AmbulanceTeam> {
	private static final String DISTANCE_KEY = "clear.repair.distance";
    private Collection<EntityID> unexploredBuildings;
    
    
    private EntityID activeTask;
    private HashMap<Integer, Integer> knownBlockades = new HashMap<Integer, Integer>();
    private List<EntityID> pathHist = new LinkedList<EntityID>();
    private final double threshold = -1; // LA-DCOP
    private final int MAX_TAREFAS = 1;
    private int maxDistance;

    private List<GeneralData> receivedTargets = new ArrayList<GeneralData>();
    private HashMap<Integer, Integer> anotherAgentTasks = new HashMap<Integer, Integer>();
    private List<GeneralData> targetsList = new ArrayList<GeneralData>();

    @Override
    public String toString() {
        return "Sample ambulance team";
    }

    @Override
    protected void postConnect() {
        super.postConnect();
        model.indexClass(StandardEntityURN.CIVILIAN, StandardEntityURN.FIRE_BRIGADE, StandardEntityURN.POLICE_FORCE, StandardEntityURN.AMBULANCE_TEAM, StandardEntityURN.REFUGE, StandardEntityURN.BUILDING);
        unexploredBuildings = new HashSet<EntityID>(buildingIDs);
        maxDistance = config.getIntValue(DISTANCE_KEY);
    }

    @Override
    protected void think(int time, ChangeSet changed, Collection<Command> heard) {
        if (time == config.getIntValue(kernel.KernelConstants.IGNORE_AGENT_COMMANDS_KEY)) {
            // Subscribe to channel 1
            sendSubscribe(time, 3);
            sendSpeak(time, 3, ("TesteCanal").getBytes());
        }

        // Verifica a lista de objetos ouvidos
        for (Command next : heard) {
            // Deve-se fazer um tratamento das mensagens recebidas e concatenar com o que for percebido
            Logger.debug("Heard " + next);
            if (next instanceof AKSpeak) {
				AKSpeak sp = (AKSpeak) next;
				Token token = new Token();
				token.decodeToken(sp.getContent(), model);
				if (token.getType() == 3) {
					Logger.debug("Adicionando tarefas recebidas");
					addTargetsToReceivedTargets(token.getData());
				} else if (token.getType() == 11) {
					GeneralData gd = token.getData().get(0);
					if (this.activeTask != null) {
						if (this.getId(this.activeTask.getValue()) == this.getId(gd.getID().getValue())) {
							if (amIMoreDistantThan(gd.getValue())) {
								Logger.debug("Estou mais distante do que " + token.getSender());
								Logger.debug("Abandonando task...");
								this.anotherAgentTasks.put(gd.getID().getValue(), 1);
								this.activeTask = null;
							}
						}
					} else {
						Logger.debug("Outro agente esta fazendo a task " + gd.getID().getValue());
						this.anotherAgentTasks.put(gd.getID().getValue(), 1);
					}
				}
            }
        }
        
        this.pathHist.add(0, me().getPosition());
        
        Logger.debug("Estou bloqueado: " + Boolean.toString(this.amIBlocked()));
        if (this.amIBlocked() && time > config.getIntValue(kernel.KernelConstants.IGNORE_AGENT_COMMANDS_KEY)) {
        	Token tk = new Token();
        	Blockade bl = getTargetBlockade();
        	
        	if (bl != null)
	        	if (this.knownBlockades.get(bl.getID().getValue()) == null) {
	                    Logger.debug("Ambulancia: Avisar policiais");
	                    tk.getData().add(new GeneralData(me().getPosition(),bl.getRepairCost()));
	                    sendSpeak(time, 2, tk.encondeToken(model, me().getID(), 2));
	                    Logger.debug("Mensagem: " + new String(tk.encondeToken(model, me().getID(), 2)));
	//                    this.knownBlockades.put(bl.getID().getValue(), 1);
	        	}
        }
        
        Logger.debug("Explorar construcoes!");

        updateUnexploredBuildings(changed, time);

        if (this.activeTask == null) {
        	if (!this.receivedTargets.isEmpty()) {
        		Logger.debug("Ambulancia: Existem tarefas oriundas de tokens.");
        		this.LADCOP(time);
        	}

        	if (!this.targetsList.isEmpty()) {
        		this.activeTask = this.targetsList.get(0).getID();
        		this.targetsList.remove(0);

        		Logger.debug("Ambulancia: Enviando token de active task.");
        		Token tk = new Token();
        		GeneralData gd = new GeneralData(this.activeTask, getPathSize(this.activeTask));
        		tk.getData().add(gd);
        		sendSpeak(time, 3, tk.encondeToken(model, me().getID(), 11));

        		doIt(time);
        		return;
        	}
        } else {
        	doIt(time);
        	return;
        }
        // se nao sabe o que fazer explora
        findCiviliansRandomly(time);

        Logger.info("Final Think");
    }

    private void findCiviliansRandomly(int time) {
        // Plan a path to a blocked area
        List<EntityID> path = search.aStarSearch(me().getPosition(), getCivilians());
        if (path != null) {
            Logger.info("Moving to target");
            sendMove(time, path);
            Logger.debug("Ambulância: Movimento aleatório -> indo para um civil");
            return;
        }
        Logger.debug("Ambulancia: Couldn't plan a path to a blocked road");
        Logger.info("Moving randomly");
        sendMove(time, randomWalk());
    }

    /**
     * Recupera a lista de civis do modelo de mundo
     * @return Lista de EntityID's dos civis
     */
    private List<EntityID> getCivilians() {
        Collection<StandardEntity> e = model.getEntitiesOfType(StandardEntityURN.CIVILIAN);
        List<EntityID> result = new ArrayList<EntityID>();
        for(StandardEntity se : e)
            result.add(se.getID());
        return result;
    }

    private void doIt(int time) {
    	if (someoneOnBoard()) {
            // Am I at a refuge?
            if (location() instanceof Refuge) {
                // Unload!
                Logger.info("Unloading");
                sendUnload(time);
                return;
            }
            else {
                // Move to a refuge
                List<EntityID> path = search.aStarSearch(me().getPosition(), refugeIDs);
                if (path != null) {
                    Logger.info("Moving to refuge");
                    sendMove(time, path);
                    return;
                }
                // What do I do now? Might as well carry on and see if we can dig someone else out.
                Logger.debug("Failed to plan path to refuge");
            }
        }
        // Go through targets (sorted by distance) and check for things we can do
        else if(model.getDistance(me().getID(), this.getId(this.activeTask.getValue())) == 0) {
            Civilian civil = (Civilian)model.getEntity(this.activeTask);
            // Targets in the same place might need rescueing or loading
            if (civil.getBuriedness() == 0 && !(location() instanceof Refuge)) {
                // Load
                Logger.info("Loading " + civil);
                sendLoad(time, civil.getID());
                return;
            }
            if (civil.getBuriedness() > 0) {
                // Rescue
                Logger.info("Rescueing " + civil);
                sendRescue(time, civil.getID());
                return;
            }
        }
        else {
            Logger.debug("Caminhando até " + this.activeTask);
            List<EntityID> path = search.aStarSearch(me().getPosition(), this.getId(this.activeTask.getValue())); // Find a path
            if (path != null) {
                Logger.debug("Found a path to task... moving.");
		sendMove(time, path);
            } 
            else Logger.debug("Caminhando até " + this.activeTask + " nao foi encontrado.");
        }
    }

    private double[] calcularMaximos(List<GeneralData> allTargets) {
		// Vetor onde guarda-se a distância e a "ardência" máxima
		double maximos[] = new double[2];
		// maximos[0] para distância e maximos[1] para "ardência"
		maximos[0] = 0;
		maximos[1] = 0;
		for (Iterator<GeneralData> it = allTargets.iterator(); it.hasNext();) {
			GeneralData gd = it.next();

			double distancia = model.getDistance(me().getID(), gd.getID());
			int tamanho = gd.getValue();
			// Caso o método getDistance retorne -1, significa que b é nulo e,
			// portanto defina a distância como 10000
			if (distancia == -1)
				distancia = 10000;

			if (distancia > maximos[0])
				maximos[0] = distancia;

			// A partir de 5 de "ardência", significa que o prédio não está
			// pegando fogo
			if (tamanho > maximos[1])
				maximos[1] = tamanho;
		}
		return maximos;
	}

    public void LADCOP(int time) {

    	int num_aceitas = 0;
    	List<GeneralData> allTargets = new ArrayList<GeneralData>();
    	List<GeneralData> notSelectedTargets = new ArrayList<GeneralData>();
    	allTargets.addAll(this.receivedTargets);
    	allTargets.addAll(this.targetsList);


		// Distância do alvo mais distante
		double[] maximos = calcularMaximos(allTargets);

		for (Iterator<GeneralData> it = allTargets.iterator(); it.hasNext();) {
			GeneralData task = it.next();
			double competencia;
			// Se só existe um alvo, sua competência é máxima (1)
			if (allTargets.size() == 1) {
				competencia = 1;
			} else {
				// Senão, calcula sua competência
				int distancia = model.getDistance(me().getID(), task.getID());

				int tamanho = task.getValue();
				// Caso o método getDistance retorne -1, significa que me() ou
				// task são null. Neste caso a competência é 0
				if (distancia == -1) {
					competencia = 0;
				}
				//
				else {
					competencia = (((maximos[0] - distancia) / maximos[0]) + ((maximos[1] - tamanho) / maximos[1])) / 2;
				}
			}
			Logger.debug("Tarefa ID=" + task.getID() + " Fieryness=" + task.getValue());
			Logger.debug("Competencia: " + competencia);

			if (threshold < competencia && num_aceitas < MAX_TAREFAS && !isAnotherAgentTask(task.getID())) {
				num_aceitas++;
				targetsList.add(task);
				Logger.info(task.getID() + " added");
			} else {
				notSelectedTargets.add(task);
				Logger.info(task.getID() + " refused");
			}
			Collections.sort(targetsList, new ShortestComparetor());
		}
		if (notSelectedTargets.size() > 0) {
			Token token = new Token();
			token.setData(notSelectedTargets);
			// comunicar aos outros agentes as tarefas NAO aceitas
			sendSpeak(time, 3, token.encondeToken(model, me().getID(), 11));
		}
		this.receivedTargets.clear();
    }

    private class ShortestComparetor implements Comparator<Object> {
		public int compare(Object o1, Object o2) {
			GeneralData b1 = (GeneralData) o1;
			GeneralData b2 = (GeneralData) o2;
			int cost1 = b1.getValue();
			int cost2 = b2.getValue();
			double d1, d2;
			d1 = model.getDistance(me().getID(), b1.getID());
			d2 = model.getDistance(me().getID(), b2.getID());
			if (d1 == -1 || d2 == -1) {
				d1 = 0;
				d2 = 0;
			}
			if (cost1 == cost2) {
				if (d1 < d2)
					return -1;
				else if (d1 > d2)
					return 1;
				else
					return 0;
			} else
				return cost1 - cost2;
		}

	}

    private boolean isAnotherAgentTask(EntityID id) {
    	return this.anotherAgentTasks.get(id.getValue()) != null;
    }
    
    private boolean amIMoreDistantThan(int hisPathSize) {

    	int myPathSize = getPathSize(this.activeTask);
		if (myPathSize > hisPathSize)
			return true;
		else return false;
    }

    private int getPathSize(EntityID id) {
    	List<EntityID> path = search
			.aStarSearch(me().getPosition(), this.getId(id.getValue()));
    	if (path != null) return path.size();
    	else return 10000;
    }

    private EntityID getId(int id) {
        return model.getEntity(new EntityID(id)).getID();
    }

    private void addTargetsToReceivedTargets(List<GeneralData> targets) {
    	for (GeneralData target : targets) {
    		for (Iterator<GeneralData> it = this.receivedTargets.iterator(); it.hasNext();) {
    			GeneralData next = it.next();
    			if (this.getId(target.getID().getValue()) == this.getId(next.getID().getValue())) {
    					it.remove();
    					break;
    			}
    		}
    		this.receivedTargets.add(target);
    	}
    }

    private boolean amIBlocked() {

    	if (this.pathHist.size() > 2) {
    		if (this.pathHist.get(0).getValue() == this.pathHist.get(1).getValue() &&
    				this.pathHist.get(1).getValue() == this.pathHist.get(2).getValue()) {
    			return true;
    		}
    	}
    	return false;
    }
    
    
    @Override
    protected EnumSet<StandardEntityURN> getRequestedEntityURNsEnum() {
        return EnumSet.of(StandardEntityURN.AMBULANCE_TEAM);
    }

    private boolean someoneOnBoard() {
        for (StandardEntity next : model.getEntitiesOfType(StandardEntityURN.CIVILIAN)) {
            if (((Human)next).getPosition().equals(getID())) {
                Logger.debug(next + " is on board");
                return true;
            }
        }
        return false;
    }

    private List<Human> getTargets() {
        List<Human> targets = new ArrayList<Human>();
        for (StandardEntity next : model.getEntitiesOfType(StandardEntityURN.CIVILIAN
                /*,StandardEntityURN.FIRE_BRIGADE, StandardEntityURN.POLICE_FORCE, StandardEntityURN.AMBULANCE_TEAM*/)) {
            Human h = (Human)next;
            if (h == me()) {
                continue;
            }
            if (h.isHPDefined()
                && h.isBuriednessDefined()
                && h.isDamageDefined()
                && h.isPositionDefined()
                && h.getHP() > 0
                && (h.getBuriedness() > 0 || h.getDamage() > 0)) {
                targets.add(h);
            }
        }
        Collections.sort(targets, new DistanceSorter(location(), model));
        return targets;
    }

    private Blockade getTargetBlockade() {
        Logger.debug("Looking for civilians");
        // location -> rece
        Area location = (Area)location();
        Logger.debug("Looking in current location");
        Blockade result = getTargetBlockade(location, maxDistance);
        if (result != null) {
            return result;
        }
        Logger.debug("Looking in neighbouring locations");
        for (EntityID next : location.getNeighbours()) {
            location = (Area)model.getEntity(next);
            result = getTargetBlockade(location, maxDistance);
            if (result != null) {
                return result;
            }
        }
        return null;
    }

    private Blockade getTargetBlockade(Area area, int maxDistance) {
        //        Logger.debug("Looking for nearest blockade in " + area);
        if (area == null || !area.isBlockadesDefined()) {
            //            Logger.debug("Blockades undefined");
            return null;
        }
        List<EntityID> ids = area.getBlockades();
        // Find the first blockade that is in range.
        int x = me().getX();
        int y = me().getY();
        for (EntityID next : ids) {
            // Verifica cada bloqueio e faz o cálculo da distância
            Blockade b = (Blockade)model.getEntity(next);
            double d = findDistanceTo(b, x, y);
            //            Logger.debug("Distance to " + b + " = " + d);
            if (maxDistance < 0 || d < maxDistance) {
                //                Logger.debug("In range");
                return b;
            }
        }
        //        Logger.debug("No blockades in range");
        return null;
    }
    
    private int findDistanceTo(Blockade b, int x, int y) {
        //        Logger.debug("Finding distance to " + b + " from " + x + ", " + y);
        List<Line2D> lines = GeometryTools2D.pointsToLines(GeometryTools2D.vertexArrayToPoints(b.getApexes()), true);
        double best = Double.MAX_VALUE;
        Point2D origin = new Point2D(x, y);
        for (Line2D next : lines) {
            Point2D closest = GeometryTools2D.getClosestPointOnSegment(next, origin);
            double d = GeometryTools2D.getDistance(origin, closest);
            //            Logger.debug("Next line: " + next + ", closest point: " + closest + ", distance: " + d);
            if (d < best) {
                best = d;
                //                Logger.debug("New best distance");
            }

        }
        return (int)best;
    }
    

    private void updateUnexploredBuildings(ChangeSet changed, int time) {
    	
    	List<GeneralData> onFireBuildings = new LinkedList<GeneralData>();
    	
        for (EntityID next : changed.getChangedEntities()) {
        	
        	if (model.getEntity(next) instanceof Building) {
	        	Building b = (Building) model.getEntity(next);
	        	
	        	// Reportar aos bombeiros se construção estiver em chamas
	        	if (b.isOnFire()) {
	        		onFireBuildings.add(new GeneralData(b.getID(), b.getFieryness()));
	        	}
        	}
        	
            unexploredBuildings.remove(next);
        }
        
        if (!onFireBuildings.isEmpty()) {
	        Token t = new Token();
	        t.setData(onFireBuildings);
	        sendSpeak(time, 1, t.encondeToken(model, me().getID(), 1));
        }
    }
}
